package org.budo.support.spring.bean.factory.lazy;

import java.util.concurrent.Callable;

import org.budo.support.javassist.util.CtMethodUtil;
import org.budo.support.lang.util.ArrayUtil;
import org.budo.support.lang.util.ReflectUtil;
import org.budo.support.lang.util.StringUtil;
import org.springframework.beans.factory.FactoryBean;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;

/**
 * @author limin
 */
public abstract class LazyInitFactoryBean<T> implements FactoryBean<T> {
    @Override
    public final boolean isSingleton() {
        return true;
    }

    @Override
    public final T getObject() throws Exception {
        Class<?> objectType = this.getObjectType();

        if (!objectType.isInterface()) {
            throw new RuntimeException(objectType.getName() + " is not interface");
        }

        Class<?> lazyInitSubclassType = this.buildSubclass(objectType);
        T newInstance = (T) ReflectUtil.newInstance(lazyInitSubclassType);

        ReflectUtil.setFieldValue(newInstance, "_targetInitializer", new Callable<T>() {
            public T call() throws Exception {
                return LazyInitFactoryBean.this.initObject();
            }
        });

        return newInstance;
    }

    public Class<?> buildSubclass(Class<?> interfaceType) {
        try {
            ClassPool classPool = ClassPool.getDefault();
            classPool.insertClassPath(new ClassClassPath(interfaceType));

            String typeName = interfaceType.getName();
            CtClass typeCtClass = classPool.get(typeName);

            String subclassName = typeName + "$$LazyInit";

            CtClass subCtClass = classPool.makeClass(subclassName);
            classPool.insertClassPath(new ClassClassPath(interfaceType));

            subCtClass.setInterfaces(new CtClass[] { typeCtClass });

            subCtClass.addField(CtField.make("java.util.concurrent.Callable _targetInitializer; ", subCtClass));

            CtMethod[] ctMethods = typeCtClass.getMethods();
            for (CtMethod ctMethod : ctMethods) {
                if (Object.class.getName().equals(ctMethod.getDeclaringClass().getName())) {
                    continue;
                }

                String delegatedMethodCode = delegatedMethodCode(ctMethod, typeName, subclassName);
                CtMethod newCtMethod = CtNewMethod.make(delegatedMethodCode, subCtClass);
                subCtClass.addMethod(newCtMethod);
            }

            return subCtClass.toClass();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static String delegatedMethodCode(CtMethod ctMethod, String typeName, String subClassName) {
        String returnType = CtMethodUtil.getReturnType(ctMethod).getName();
        String methodName = ctMethod.getName();

        String parameterList = CtMethodUtil.getParameters(ctMethod);
        String exceptions = CtMethodUtil.getExceptions(ctMethod);

        CtClass[] parameterTypes = CtMethodUtil.getParameterTypes(ctMethod);
        String parameterReference = StringUtil.join(ArrayUtil.sequence(1, parameterTypes.length), ", ", CtMethodUtil.$_PARAMETER_PREFIX, "");

        String src = " public " + returnType + " " + methodName + "(" + parameterList + ") " + exceptions + " { ";//
        src += ("void".equals(returnType) ? "" : " return ") //
                + " ((" + typeName + ")_targetInitializer.call())." + methodName + "(" + parameterReference + "); ";
        src += " } ";
        return src;
    }

    public abstract T initObject();
}